# 算法链与管道
"""
对于许多机器学习算法，你提供的特定数据表示非常重要，正如第 4 章中所述。我们在第
3 章中讲过，首先对数据进行缩放，然后手动合并特征，再利用无监督机器学习来学习特
征。因此，大多数机器学习应用不仅需要应用单个算法，而且还需要将许多不同的处理步
骤和机器学习模型链接在一起。本章将介绍如何使用 Pipeline 类来简化构建变换和模型链
的过程。我们将重点介绍如何将 Pipeline 和 GridSearchCV 结合起来，从而同时搜索所有
处理步骤中的参数。

"""
import mglearn
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.svm import SVC

'''
举一个例子来说明模型链的重要性。我们知道，可以通过使用 MinMaxScaler 进行预处理来
大大提高核 SVM 在 cancer 数据集上的性能。下面这些代码实现了划分数据、计算最小值
和最大值、缩放数据与训练 SVM：
'''

# 加载并划分数据
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
 cancer.data, cancer.target, random_state=0)
# 计算训练数据的最小值和最大值
scaler = MinMaxScaler().fit(X_train)

# 对训练数据进行缩放
X_train_scaled = scaler.transform(X_train)
svm = SVC()
# 在缩放后的训练数据上学习SVM
svm.fit(X_train_scaled, y_train)
# 对测试数据进行缩放，并计算缩放后的数据的分数
X_test_scaled = scaler.transform(X_test)
print("Test score: {:.2f}".format(svm.score(X_test_scaled, y_test)))
# Test score: 0.97

# 用预处理进行参数选择
'''
现在，假设我们希望利用 GridSearchCV 找到更好的 SVC 参数，正如第 5 章中所做的那样。
我们应该怎么做？一种简单的方法可能如下所示：
'''
# 只是为了便于说明，不要在实践中使用这些代码！
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100],
 'gamma': [0.001, 0.01, 0.1, 1, 10, 100]}
grid = GridSearchCV(SVC(), param_grid=param_grid, cv=5)
grid.fit(X_train_scaled, y_train)
print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Best set score: {:.2f}".format(grid.score(X_test_scaled, y_test)))
print("Best parameters: ", grid.best_params_)
# Best cross-validation accuracy: 0.98
# Best set score: 0.97
# Best parameters:  {'C': 1, 'gamma': 1}

'''
这里我们利用缩放后的数据对 SVC 参数进行网格搜索。但是，上面的代码中有一个不易察
觉的陷阱。在缩放数据时，我们使用了训练集中的所有数据来找到训练的方法。然后，我
们使用缩放后的训练数据来运行带交叉验证的网格搜索。对于交叉验证中的每次划分，原
始训练集的一部分被划分为训练部分，另一部分被划分为测试部分。测试部分用于度量在
训练部分上所训练的模型在新数据上的表现。但是，我们在缩放数据时已经使用过测试部
分中所包含的信息。请记住，交叉验证每次划分的测试部分都是训练集的一部分，我们使
用整个训练集的信息来找到数据的正确缩放。
对于模型来说，这些数据与新数据看起来截然不同。如果我们观察新数据（比如测试集中
的数据），那么这些数据并没有用于对训练数据进行缩放，其最大值和最小值也可能与训
练数据不同。下面这个例子（图 6-1）显示了交叉验证与最终评估这两个过程中数据处理
的不同之处：
'''

mglearn.plots.plot_improper_processing()

'''
因此，对于建模过程，交叉验证中的划分无法正确地反映新数据的特征。我们已经将这部
分数据的信息泄露（leak）给建模过程。这将导致在交叉验证过程中得到过于乐观的结果，
并可能会导致选择次优的参数。
为了解决这个问题，在交叉验证的过程中，应该在进行任何预处理之前完成数据集的划
分。任何从数据集中提取信息的处理过程都应该仅应用于数据集的训练部分，因此，任何
交叉验证都应该位于处理过程的“最外层循环”。
在 scikit-learn 中，要想使用 cross_val_score 函数和 GridSearchCV 函数实现这一点，可
以使用 Pipeline 类。Pipeline 类可以将多个处理步骤合并（glue）为单个 scikit-learn 估
计器。Pipeline 类本身具有 fit、predict 和 score 方法，其行为与 scikit-learn 中的其
他模型相同。Pipeline 类最常见的用例是将预处理步骤（比如数据缩放）与一个监督模型
（比如分类器）链接在一起。
'''

# 构建管道
'''
我们来看一下如何使用 Pipeline 类来表示在使用 MinMaxScaler 缩放数据之后再训练一个
SVM 的工作流程（暂时不用网格搜索）。首先，我们构建一个由步骤列表组成的管道对象。
每个步骤都是一个元组，其中包含一个名称（你选定的任意字符串 1
）和一个估计器的实例：
'''

pipe = Pipeline([("scaler",MinMaxScaler()),("svm",SVC())])
# 这里我们创建了两个步骤：第一个叫作 "scaler"，是 MinMaxScaler 的实例；第二个叫作
# "svm"，是 SVC 的实例。现在我们可以像任何其他 scikit-learn 估计器一样来拟合这个管道：
pipe.fit(X_train, y_train)
# 这里 pipe.fit 首先对第一个步骤（缩放器）调用 fit，然后使用该缩放器对训练数据进
# 行变换，最后用缩放后的数据来拟合 SVM。要想在测试数据上进行评估，我们只需调用
# pipe.score：

print("Test score: {:.2f}".format(pipe.score(X_test, y_test)))
# Test score: 0.97

'''
如果对管道调用 score 方法，则首先使用缩放器对测试数据进行变换，然后利用缩放后
的测试数据对 SVM 调用 score 方法。如你所见，这个结果与我们从本章开头的代码得到
的结果（手动进行数据变换）是相同的。利用管道，我们减少了“预处理 + 分类”过程
所需要的代码量。但是，使用管道的主要优点在于，现在我们可以在 cross_val_score 或
GridSearchCV 中使用这个估计器。
'''

# 在网格搜索中使用管道
'''
在网格搜索中使用管道的工作原理与使用任何其他估计器都相同。我们定义一个需要搜索
的参数网格，并利用管道和参数网格构建一个 GridSearchCV。不过在指定参数网格时存在
一处细微的变化。我们需要为每个参数指定它在管道中所属的步骤。我们要调节的两个参
数 C 和 gamma 都是 SVC 的参数，属于第二个步骤。我们给这个步骤的名称是 "svm"。为管
道定义参数网格的语法是为每个参数指定步骤名称，后面加上 __（双下划线），然后是参
数名称。因此，要想搜索 SVC 的 C 参数，必须使用 "svm__C" 作为参数网格字典的键，对
gamma 参数也是同理：
'''
param_grid = {'svm__C': [0.001, 0.01, 0.1, 1, 10, 100],
 'svm__gamma': [0.001, 0.01, 0.1, 1, 10, 100]}

# 有了这个参数网格，我们可以像平常一样使用 GridSearchCV：
grid = GridSearchCV(pipe, param_grid=param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best cross-validation accuracy: {:.2f}".format(grid.best_score_))
print("Test set score: {:.2f}".format(grid.score(X_test, y_test)))
print("Best parameters: {}".format(grid.best_params_))

# Best cross-validation accuracy: 0.98
# Test set score: 0.97
# Best parameters: {'svm__C': 1, 'svm__gamma': 1}

'''
与前面所做的网格搜索不同，现在对于交叉验证的每次划分来说，仅使用训练部分对
MinMaxScaler 进行拟合，测试部分的信息没有泄露到参数搜索中。将图 6-2 与图 6-1 进行对比。
'''
mglearn.plots.plot_proper_processing()

'''
在交叉验证中，信息泄露的影响大小取决于预处理步骤的性质。使用测试部分来估计数据
的范围，通常不会产生可怕的影响，但在特征提取和特征选择中使用测试部分，则会导致
结果的显著差异。
'''

# 通用的管道接口
'''
Pipeline 类不但可用于预处理和分类，实际上还可以将任意数量的估计器连接在一起。例
如，你可以构建一个包含特征提取、特征选择、缩放和分类的管道，总共有 4 个步骤。同
样，最后一步可以用回归或聚类代替分类。
对于管道中估计器的唯一要求就是，除了最后一步之外的所有步骤都需要具有 transform
方法，这样它们可以生成新的数据表示，以供下一个步骤使用。
在调用 Pipeline.fit 的过程中，管道内部依次对每个步骤调用 fit 和 transform2
，其输入
是前一个步骤中 transform 方法的输出。对于管道中的最后一步，则仅调用 fit。
忽略某些细枝末节，其实现方法如下所示。请记住，pipeline.steps 是由元组组成的列表，
所以 pipeline.steps[0][1] 是第一个估计器，pipeline.steps[1][1] 是第二个估计器，以
此类推：
'''
def fit(self, X, y):
 X_transformed = X
 for name, estimator in self.steps[:-1]:
     # 遍历除最后一步之外的所有步骤
     # 对数据进行拟合和变换
     X_transformed = estimator.fit_transform(X_transformed, y)
     # 对最后一步进行拟合
 self.steps[-1][1].fit(X_transformed, y)
 return self

# 使用 Pipeline 进行预测时，我们同样利用除最后一步之外的所有步骤对数据进行变换
# （transform），然后对最后一步调用 predict：

def predict(self, X):
 X_transformed = X
 for step in self.steps[:-1]:
     # 遍历除最后一步之外的所有步骤
     # 对数据进行变换
     X_transformed = step[1].transform(X_transformed)
 # 利用最后一步进行预测
 return self.steps[-1][1].predict(X_transformed)

# 用make_pipeline方便地创建管道
'''
利用上述语法创建管道有时有点麻烦，我们通常不需要为每一个步骤提供用户指定的名
称。有一个很方便的函数 make_pipeline，可以为我们创建管道并根据每个步骤所属的类
为其自动命名。make_pipeline 的语法如下所示：
'''

# 标准语法
pipe_long = Pipeline([("scaler", MinMaxScaler()), ("svm", SVC(C=100))])
# 缩写语法
pipe_short = make_pipeline(MinMaxScaler(), SVC(C=100))

'''
管道对象 pipe_long 和 pipe_short 的作用完全相同，但 pipe_short 的步骤是自动命名的。
我们可以通过查看 steps 属性来查看步骤的名称：
'''

print("Pipeline steps:\n{}".format(pipe_short.steps))
# Pipeline steps:
# [('minmaxscaler', MinMaxScaler()), ('svc', SVC(C=100))]

# 访问步骤属性
'''
通常来说，你希望检查管道中某一步骤的属性——比如线性模型的系数或 PCA 提取的成
分。要想访问管道中的步骤，最简单的方法是通过 named_steps 属性，它是一个字典，将
步骤名称映射为估计器：
'''
# 用前面定义的管道对cancer数据集进行拟合
pipe.fit(cancer.data)
# 从"pca"步骤中提取前两个主成分
components = pipe.named_steps["pca"].components_
print("components.shape: {}".format(components.shape))

# 访问网格搜索管道中的属性
'''
本章前面说过，使用管道的主要原因之一就是进行网格搜索。一个常见的任务是在网格搜
索内访问管道的某些步骤。我们对 cancer 数据集上的 LogisticRegression 分类器进行网
格搜索，在将数据传入 LogisticRegression 分类器之前，先用 Pipeline 和 StandardScaler
对数据进行缩放。首先，我们用 make_pipeline 函数创建一个管道：
'''
pipe = make_pipeline(StandardScaler(), LogisticRegression())

'''
接下来，我们创建一个参数网格。我们在第 2 章中说过，LogisticRegression 需要调节的
正则化参数是参数 C。我们对这个参数使用对数网格，在 0.01 和 100 之间进行搜索。由于
我们使用了 make_pipeline 函数，所以管道中 LogisticRegression 步骤的名称是小写的类
名称 logisticregression。因此，为了调节参数 C，我们必须指定 logisticregression__C
的参数网格：
'''

param_grid = {'logisticregression__C': [0.01, 0.1, 1, 10, 100]}

# 像往常一样，我们将 cancer 数据集划分为训练集和测试集，并对网格搜索进行拟合：
X_train, X_test, y_train, y_test = train_test_split(
 cancer.data, cancer.target, random_state=4)
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)

'''
那么我们如何访问 GridSearchCV 找到的最佳 LogisticRegression 模型的系数呢？我们从第
5 章中知道，GridSearchCV 找到的最佳模型（在所有训练数据上训练得到的模型）保存在
grid.best_estimator_ 中：
'''

print("Best estimator:\n{}".format(grid.best_estimator_))

'''
在 我 们 的 例 子 中，best_estimator_ 是 一 个 管 道， 它 包 含 两 个 步 骤：standardscaler
和 logisticregression。 如 前 所 述， 我 们 可 以 使 用 管 道 的 named_steps 属性来访问
logisticregression 步骤：
'''
print("Logistic regression step:\n{}".format(
 grid.best_estimator_.named_steps["logisticregression"]))

'''
现在我们得到了训练过的 LogisticRegression 实例，下面我们可以访问与每个输入特征相
关的系数（权重）
'''

print("Logistic regression coefficients:\n{}".format(
 grid.best_estimator_.named_steps["logisticregression"].coef_))


# 网格搜索选择使用哪个模型
'''
你甚至可以进一步将 GridSearchCV 和 Pipeline 结合起来：还可以搜索管道中正在执行的
实际步骤（比如用 StandardScaler 还是用 MinMaxScaler）。这样会导致更大的搜索空间，
应该予以仔细考虑。尝试所有可能的解决方案，通常并不是一种可行的机器学习策略。但
下面是一个例子：在 iris 数据集上比较 RandomForestClassifier 和 SVC。我们知道，SVC
可能需要对数据进行缩放，所以我们还需要搜索是使用 StandardScaler 还是不使用预处
理。我们知道，RandomForestClassifier 不需要预处理。我们先定义管道。这里我们显式
地对步骤命名。我们需要两个步骤，一个用于预处理，然后是一个分类器。我们可以用
SVC 和 StandardScaler 来将其实例化：
'''

pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])

'''
现在我们可以定义需要搜索的parameter_grid。我们希望classifier 是 RandomForestClassifier
或 SVC。由于这两种分类器需要调节不同的参数，并且需要不同的预处理，所以我们可以
使用 5.2.3 节“在非网格的空间中搜索”中所讲的搜索网格列表。为了将一个估计器分配
给一个步骤，我们使用步骤名称作为参数名称。如果我们想跳过管道中的某个步骤（例
如，RandomForest 不需要预处理），则可以将该步骤设置为 None：
'''
param_grid = [
 {'classifier': [SVC()], 'preprocessing': [StandardScaler(), None],
 'classifier__gamma': [0.001, 0.01, 0.1, 1, 10, 100],
 'classifier__C': [0.001, 0.01, 0.1, 1, 10, 100]},
 {'classifier': [RandomForestClassifier(n_estimators=100)],
 'preprocessing': [None], 'classifier__max_features': [1, 2, 3]}]

# 现在，我们可以像前面一样将网格搜索实例化并在 cancer 数据集上运行：
X_train, X_test, y_train, y_test = train_test_split(
 cancer.data, cancer.target, random_state=0)
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X_train, y_train)
print("Best params:\n{}\n".format(grid.best_params_))
print("Best cross-validation score: {:.2f}".format(grid.best_score_))
print("Test-set score: {:.2f}".format(grid.score(X_test, y_test)))
